9.4.1 创建马赛克图片

创建马赛克图片的第一步是定义一个马赛克算法,下面是一个无需使用任何第三方库的算法步骤。

(1)通过扫描图片目录,并使用图片的文件名作为键、图片的平均颜色作为值,构建出一个由瓷砖图片组成的散列,也就是一个瓷砖图片数据库。通过计算图片中每个像素红、绿、蓝3种颜色的总和,并将它们除以像素的总数量,我们就得到了一个三元组,而这个三元组就是图片的平均颜色。

(2)根据瓷砖图片的大小,将目标图片切割成一系列尺寸更小的子图片。

(3)对于目标图片切割出的每张子图片,将它们位于左上方的第一个像素设定为该图片的平均颜色。

(4)根据子图片的平均颜色,在瓷砖图片数据库中找出一张平均颜色与之最为接近的瓷砖图片,然后在目标图片的相应位置上使用瓷砖图片去代替原有的子图片。为了找出最接近的平均颜色,程序需要将子图片的平均颜色以及瓷砖图片的平均颜色都转换成三维空间中的一个点,并计算这两点之间的欧几里得距离。

(5)当一张瓷砖图片被选中之后,程序就会把这张图片从瓷砖图片数据库中移除,以此来保证马赛克图片中的每张瓷砖图片都是独一无二、各不相同的。

文件 mosaic.go 实现了上述的马赛克算法,我们接下来将逐一分析该文件包含的各个函数。首先,代码清单9-11展示了该文件中用于计算平均颜色的 averageColor 函数。

代码清单9-11  averageColor 函数

func averageColor(img image.Image) [3]float64 {
 bounds := img.Bounds()
 r, g, b := 0.0, 0.0, 0.0
 for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
  for x := bounds.Min.X; x < bounds.Max.X; x++ {
   r1, g1, b1, _ := img.At(x, y).RGBA()
   r, g, b = r+float64(r1), g+float64(g1), b+float64(b1)  ❶
  }
 }
 totalPixels := float64(bounds.Max.X * bounds.Max.Y)
 return [3]float64{r / totalPixels, g / totalPixels, b / totalPixels}
}

❶ 计算出给定图片的平均颜色

averageColor 函数会把给定图片的每个像素中的红、绿、蓝3种颜色相加起来,并将这些颜色的总和除以图片的像素数量,最后把除法计算的结果记录在一个新创建的三元组里面(这个三元组使用包含3个元素的数组表示)。

之后,程序会使用代码清单9-12所示的 resize 函数,把图片缩放至指定的宽度。

代码清单9-12  resize 函数

func resize(in image.Image, newWidth int) image.NRGBA { ❶
 bounds := in.Bounds()
 ratio := bounds.Dx()/ newWidth
 out := image.NewNRGBA(image.Rect(bounds.Min.X/ratio, bounds.Min.X/ratio,
➥bounds.Max.X/ratio, bounds.Max.Y/ratio))
 for y, j := bounds.Min.Y, bounds.Min.Y; y < bounds.Max.Y; y, j = y+ratio,
 j+1 {
  for x, i := bounds.Min.X, bounds.Min.X; x < bounds.Max.X; x, i =
  ➥x+ratio, i+1 {
   r, g, b, a := in.At(x, y).RGBA()
   out.SetNRGBA(i, j, color.NRGBA{uint8(r>>8), uint8(g>>8), uint8(b>>8),
   ➥uint8(a>>8)})
  }
 }
 return *out
}

❶ 将给定图片缩放至指定宽度

代码清单9-13展示了 tilesDB 函数,这个函数会通过扫描瓷砖图片所在的目录来创建一个瓷砖图片数据库。

代码清单9-13  tilesDB 函数

func tilesDB() map[string][3]float64 {  ❶
   fmt.Println("Start populating tiles db ...")
 db := make(map[string][3]float64)
 files, _ := ioutil.ReadDir("tiles")
 for _, f := range files {
  name := "tiles/" + f.Name()
  file, err := os.Open(name)
  if err == nil {
   img, _, err := image.Decode(file)
   if err == nil {
    db[name] = averageColor(img)
   } else {
    fmt.Println("error in populating TILEDB:", err, name)
   }
  } else {
   fmt.Println("cannot open file", name, err)
  }
  file.Close()
 }
 fmt.Println("Finished populating tiles db.")
 return db
}

❶ 在内存中创建一个瓷砖图片数据库

瓷砖图片数据库是一个映射,这个映射的键为字符串,而值则为三元组(在程序中使用包含3个元素的数组来表示)。 tilesDB 函数会打开目录中的每张图片,并根据这些图片的平均颜色在映射中创建相应的记录。为了寻找与目标图片相匹配的瓷砖图片,程序会将 tilesDB 函数创建的瓷砖图片数据库以及目标图片的平均颜色传入 nearest 函数。

func nearest(target [3]float64, db *map[string][3]float64) string {  ❶
 var filename string
 smallest := 1000000.0
 for k, v := range *db {
  dist := distance(target, v)
  if dist < smallest {
   filename, smallest = k, dist
  }
 }
 delete(*db, filename)
 return filename
}

❶ 寻找与目标图片平均颜色最接近的瓷砖图片

nearest 函数会把瓷砖图片数据库中的所有记录与目标图片的平均颜色一一进行对比,而两者欧几里得距离最短的那一条记录,就是与目标图片平均颜色最为接近的瓷砖图片。函数会从数据库中移除被选中的瓷砖图片,并把该图片的名字返回给调用者。代码清单9-14展示了用于计算两个三元组之间的欧几里得距离的 distance 函数。

代码清单9-14  distance 函数

func distance(p1 [3]float64, p2 [3]float64) float64 { ❶
 return math.Sqrt(sq(p2[0]-p1[0]) + sq(p2[1]-p1[1]) + sq(p2[2]-p1[2]))
}
func sq(n float64) float64 { ❷
 return n * n
}

❶ 计算两点之间的欧几里得距离

❷ 计算给定数值的平方

因为扫描和载入瓷砖图片数据库是一项非常花时间的操作,所以为了效率起见,比起每次生成马赛克图片的时候都重复一遍这个操作,更合理的做法是只执行一次这个操作,创建出一个瓷砖图片数据库的原本(source),然后在每次生成马赛克图片的时候都根据这个原本复制出一个独立的副本(clone)。代码清单9-15展示了作为瓷砖图片数据库的原本而存在的 TILEDB 全局变量,Web应用在启动的时候就会创建并填充这个变量。

代码清单9-15  cloneTilesDB 函数

var TILESDB map[string][3]float64
func cloneTilesDB() map[string][3]float64 { ❶
 db := make(map[string][3]float64)
 for k, v := range TILESDB {
  db[k] = v
 }
 return db
}

❶ 每次需要生成马赛克图片的时候,就复制出一个瓷砖图片数据库副本

results matching ""

    No results matching ""